/*
 * Copyright 2002-2021 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.fjsei.yewu.inspect;

import com.querydsl.core.BooleanBuilder;
import graphql.relay.Connection;
import graphql.schema.DataFetchingEnvironment;
import lombok.extern.slf4j.Slf4j;
import md.cm.base.Companies;
import md.cm.flow.ApprovalStm;
import md.cm.flow.ApprovalStmRepository;
import md.cm.geography.Country;
import md.cm.geography.Province;
import md.cm.unit.*;
import md.specialEqp.BusinessCat_Enum;
import md.specialEqp.Eqp;
import md.specialEqp.Report;
import md.specialEqp.ReportRepository;
import md.specialEqp.fee.Charging;
import md.specialEqp.inspect.*;
import md.specialEqp.type.PipingUnit;
import md.system.Ifop_Enu;
import md.system.User;
import org.fjsei.yewu.exception.CommonGraphQLException;
import org.fjsei.yewu.graphql.DbPageConnection;
import org.fjsei.yewu.graphql.IdMapper;
import org.fjsei.yewu.input.FeeItemInput;
import org.fjsei.yewu.input.TaskDetailInput;
import org.fjsei.yewu.jpa.PageOffsetFirst;
import org.fjsei.yewu.payload.CudFeeItemResp;
import org.fjsei.yewu.payload.TaskComResp;
import org.fjsei.yewu.resolver.Comngrql;
import org.fjsei.yewu.util.Tool;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.MutationMapping;
import org.springframework.graphql.data.method.annotation.SchemaMapping;
import org.springframework.graphql.execution.BatchLoaderRegistry;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import java.text.ParseException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;

import static graphql.Assert.assertNotNull;
import static graphql.Assert.assertTrue;
import static java.lang.String.format;

/**默认Unit模型的属性； graphQL接口;
 * 底下批处理方案还不是一种的。 注释掉的也是1个方案。
 * 每一个graphQL的Object对象type都需单独声明一个XxxController implements IdMapper<type> {} 否则Id都无法转换成Relay要的GlobalID的。
 * */
@Slf4j
@Controller
public class TaskController extends Comngrql implements IdMapper<Task> {
	@PersistenceContext(unitName = "entityManagerFactorySei")
	private EntityManager emSei;                //EntityManager相当于hibernate.Session：
	private final Units units;
	private final Companies companies;
	private final TaskRepository tasks;
	private final IspRepository isps;
	private final ApprovalStmRepository  approvalStms;
	private final ReportRepository  reports;
	private final DetailRepository  details;
	/** registry参数：IDEA报红却能运行成功，实际运行注入DefaultBatchLoaderRegistry;
     *  消除报红，测试平台Units没有报红 https://blog.csdn.net/zy103118/article/details/84523452
	 * 添加了 @ComponentScan({"md","org.fjsei.yewu"}) 以后: Units报红消除了。
	 * */
	public TaskController(BatchLoaderRegistry registry, Units units, Companies companies, TaskRepository tasks, IspRepository isps, ApprovalStmRepository approvalStms, ReportRepository reports, DetailRepository details) {
		this.units = units;
		this.companies = companies;
		this.tasks = tasks;
		this.isps = isps;
		this.approvalStms = approvalStms;
		this.reports = reports;
		this.details = details;
	}
	/**
	 * 生成任务 ipsu检验机构默认user所属单位。 String servuId,对应@Autowired  Unit unit,
	 * 尝试失败：无法手动id加载实体！也就是函数参数用@Autowired  Unit servu=没法用的。
	 * date,bsType,entrust,servuId前面这四个接口参数是必须提供的；后面4个参数可选输入的。
	 * @param servuId ：是服务单位,输入时用关联的PersonES,CompanyES,Unit类型三种都可以的。 不能用String[] devs来接收的。
	 * */
	@MutationMapping
	@Transactional
	public  Task addTask(@Argument LocalDate date, @Argument BusinessCat_Enum bsType, @Argument Boolean entrust, @Argument String servuId,
						 @Argument String depId, @Argument String officeId, @Argument String liablerId, @Argument List<String> devs) {
		User user=checkAuth();
		Assert.isTrue(null!=user, "未授权");
	//	Tool.ResolvedGlobalId gId;
		//不能简单地用Unit servUnit =entityOf(servuId,Unit.class);
		Unit servUnit=fromInputUnitGlobalID(servuId);
		//【注意】servuId代表的PersonES有可能不存在对应的Unit啊，有些Person仅为了注册User平台账户而增加，不算在特种设备单位概念的之中。
		Assert.isTrue(servUnit != null, "未找到serv Unit:" + servuId);
		Task task = new Task();
		task.setDate(date);
		task.setBsType(bsType);
		task.setEntrust(entrust);
		task.setServu(servUnit);
		//todo: 爆缓慢： @ToString导致的。
		task.setIspu(user.getUnit());    //本单位的任务 "福建省特种设备检验研究院"
		User liabler=entityOf(liablerId,User.class);
		if (null != liabler) {
			task.setLiabler(liabler);
			task.setOffice(liabler.getOffice());    //最低优先级承认
			task.setDep(liabler.getDep());
		}
		Office office=entityOf(officeId,Office.class);
		if (null != office) {
			task.setOffice(office);
			task.setDep(office.getDep());    //中等优先级承认
		}
		Division division=entityOf(depId,Division.class);
		if (null != division) {
			task.setDep(division);     //最高优先级承认, 覆盖上面的设置
		}
		//直接关联设备多个；
		if(null!=devs) {         //Java比起JS啰嗦了！
			for (String devId : devs) {
				Eqp eqp = entityOf(devId, Eqp.class);
				if (null == eqp) throw new CommonGraphQLException("未找到Eqp:" + devId, devId);
				Isp isp = new Isp();
				isp.setDev(eqp);
				Detail detail = new Detail();
				//若光用detail.setIsp(isp);就无法实际关联上的！
				isp.setBus(detail);      //1:1关系维护方是Isp表。
				detail.setIsp(isp);      //若省略这行就导致：return task{无法内省出来.detail.isp==null}
				detail.setTask(task);
				task.getDets().add(detail);
				isps.save(isp);
				details.save(detail);
				//【立刻更新需要】在前端上无法立刻更新，看不见新生成任务；加底下2行点刷新URL可立刻看见。
				//eQP.getIsps().add(isp);
				//eQPRepository.save(eQP);
			}
		}
		//Set<Isp> isps= new HashSet<>();   List<Isp> isps=new ArrayList<>();
		tasks.save(task);
		return task;
	}

	/**必须改成 @Controller 否则后端毫无报错。 参数@Argument名字必须和graphQL模型定义一致！
	 * 若用@Argument UUID task, 是能够直接接收前端用字符串发来的ID的。
	 * 任务的部门归属划分：在其它渠道#批处理环节的，去指定 #区域，受理。
	 * */
	@MutationMapping
	@Transactional
	public Task dispatchToOffice(@Argument String task, @Argument String office)
	{
		User user=checkAuth();		 //([权限名id || or 权限名 ''2 #defined QX_NAME_MANAGER]);   && user2==()并且第二种角色的()
		Assert.notNull(user, "未授权");
		//todo:角色权限控制
		Task taskEnt = entityOf(task,Task.class);
		Assert.notNull(taskEnt, "未找到task"+task);
		Office officeEnt = entityOf(office,Office.class);
		Assert.notNull(officeEnt, "未找到Office:"+office);
		Assert.isTrue(user.getUnit()==taskEnt.getIspu(),"非本单位");
		Assert.isTrue(officeEnt.getDep() == taskEnt.getDep() || null== taskEnt.getDep(),"部门错:"+taskEnt.getDep().getName());
		Assert.isTrue(TaskState_Enum.DEPART == taskEnt.getStatus() || TaskState_Enum.INIT == taskEnt.getStatus(),"任务状态错");
		taskEnt.setOffice(officeEnt);
		if(null== taskEnt.getDep())	 	taskEnt.setDep(officeEnt.getDep());
		//devs.forEach(dev -> dev.getTask().remove(task));
		taskEnt.setStatus(TaskState_Enum.OFFICE);
		tasks.save(taskEnt);
		return taskEnt;
	}
	/**分配给负责人时 直接录入建议的报告编号--有多个设备的还需说明以及尾数递进增加数值范围 @001@ ，用户负责no规范性。
	 * 满足@@所括弧进的最少1个数数字。 @@所包括的数字位个数只能多不能少。
	 * 允许多次运行：缺省值取前面已设置的参数。no若存在未分配编号的Isp就必须提供。
	 * 这个报告编号no分配完就不再修改，要改只能后台人工上。万一删除了主报告忘记了旧的no编码？可再次补回新的no。
	 * 每个特检院自己负责编码，ispUnit不同特检院的报告编码可以相同的。本检验单位之内的报告号不得重复，最好年份+科室代码+序号从小到大增长+报告模板的编码缩写,单位自己记住滚动的序号。
	 * */
	@MutationMapping
	@Transactional
	public Task dispatchToLiabler(@Argument("task") String taskId,@Argument("liabler") String liablerId,@Argument String nos)
	{
		User user=checkAuth();
		Assert.notNull(user, "未授权");
		Task task = entityOf(taskId,Task.class);
		Assert.isTrue(task != null,"未找到task:"+taskId);
		User liabler = entityOf(liablerId,User.class);
		Assert.notNull(liabler, "未找到user:"+liablerId);
		Assert.isTrue(user.getOffice()==task.getOffice(),"非本科室");
		Assert.isTrue(liabler.getOffice()==task.getOffice(),"非本科室责任人");
		Assert.isTrue(TaskState_Enum.OFFICE == task.getStatus() || TaskState_Enum.PERSON == task.getStatus(),"任务状态错");
		task.setLiabler(liabler);
		task.setStatus(TaskState_Enum.PERSON);
		//每个Isp-Detail:都有各自唯一一个对应报告编号no; 后续星新添加Detail-Eqp进来的话，需要再次设置no但仅仅针对新加的还未设置isp.no的那些Isp报告才适用。存在no的不要改。
		if(StringUtils.hasText(nos)) {
			boolean isSerial = false;        //输入编号:是否@001@格式的
			long serial = 0;            //中间的纯数字序号
			String head = "";
			String tail = "";
			int digits = 1;        //@@中间包括的递增数字序列串的位数
			String[] splits = nos.split("@");        //相反的 String.join("-", "Java", )
			Assert.isTrue(splits.length <= 3, "编号违规:" + nos);
			if (splits.length >= 2) {
				isSerial = true;
				head = splits[0];
				String numsrial = splits[1];
				try {
					serial = Long.parseLong(numsrial);
				} catch (NumberFormatException ex) {
					throw new CommonGraphQLException("非序列号 " + numsrial);
				}
				digits = numsrial.length();
				if (splits.length >= 3)
					tail = splits[2];
			}
			String fmtstrCnt = String.format("%d", digits);
			//序号越界：比如 nos=“XF38@999@GB092”，就存在越界可能:实际上第三个设备的检验编号生成结果会是“XF381001GB092”：就是@@中间数字3个数字扩充成了4个位数数字；可能不符合预期？#把输入nos改成“XF3@8999@GB092”结果就符合。
			String fmtstr = "%s%0" + fmtstrCnt + "d%s";
			//IDEA自动添加的代码： 太啰嗦了：这个  ()->{}  并发多线程？
			AtomicInteger num = new AtomicInteger(0);
			boolean finalIsSerial = isSerial;
			long finalSerial = serial;
			String finalHead = head;
			String finalTail = tail;
			task.getDets().forEach(bus -> {
				Assert.notNull(bus.getIsp(), "没isp,det:"+bus.getId());		//只能放弃Isp-Detail;
				if (!StringUtils.hasText(bus.getIsp().getNo())) {
					if (finalIsSerial) {
						long myNum = finalSerial + num.get();		//递增序号++
						String repNo = String.format(fmtstr, finalHead, myNum, finalTail);
						bus.getIsp().setNo(repNo);
						num.getAndIncrement();
					} else
						bus.getIsp().setNo(nos);
				}
			});
			tasks.save(task);
		}
		else{
			//【前提】有Detail的必然配套有Isp,手动创建isp==null问题。
			long counter=task.getDets().stream().filter(detail -> !StringUtils.hasText(detail.getIsp().getNo()) ).count();
			if(counter>0)
				throw new CommonGraphQLException("缺报告编号:" + counter+ "个设备检验", task.getId());
		}
		return task;
	}
	/**责任人派工 : 非常接近报告编辑的时间点。
	 *注意verify 在*.graphqls用的是ID类型，java函数直接使用User也能够直接给你User("name=ID!")，靠！
	 * 前端发后端verify:{id:1,username:'herzhang'},后端实体User(String name){}会直接构造输入参数，把{id=1, username=herzhang}当成name构造new User();
	 * graphQL把{id:1,username:'herzhang'}当成ID!，然后new构造User(name=ID)对给底层java接口函数。
	 * 对于graphql的ID类型的输入声明：前端用对象直接做该参数发过来了,dispatchTaskTo(User verify)会直接new User(String)实例出来，而非数据库的真实记录。
	 * 报错：codes [typeMismatch.taskDate,typeMismatch,typeMismatch.java.time.LocalDate]； *.graphqls模型定义文件要改,taskDate: Date!,
	 * */
	@MutationMapping
	@Transactional
	public Task dispatchTaskTo(@Argument String id,@Argument LocalDate taskDate,@Argument("verify") String verifyId,@Argument("ispmen") List<String> ispmenIds,
							    @Argument String reviewerId,@Argument String approverId,@Argument String modeltype,@Argument Short modelversion) throws ParseException
	{
		//if(!emSei.isJoinedToTransaction())      emSei.joinTransaction();
		User user=checkAuth();
		Assert.notNull(user, "未授权");
		Task task = entityOf(id,Task.class);
		Assert.notNull(task, "未找到task:"+task);
		Assert.isTrue(user==task.getLiabler(),"非责任本人");
		//考虑：重复派工对诸如 流程引擎 这样的非幂等操作的影响。
		Assert.isTrue(TaskState_Enum.PERSON == task.getStatus() || TaskState_Enum.DISP == task.getStatus(),"任务状态错");
		User verify = entityOf(verifyId,User.class);		//校核人 负责启动zeebe流转; 绩效与统计按照Task责任人科室计算的，和具体干活人独立开，流程人和Task责任人无关。
		Assert.notNull(verify, "未找到user:"+verifyId);

		List<User> ispMens=  new ArrayList<>();		//Set<User> ispMen= new HashSet<User>();
		ispmenIds.stream().forEach(item -> {
			User userisp = entityOf(item,User.class);
			//todo: 检验人员资质的检查：
			Assert.notNull(userisp, "未找到user:"+item);
			ispMens.add(userisp);
		});
		/*  SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
			task.setDate(sdf.parse(taskDate));	*/
		task.setDate(taskDate);
		User reviewer = entityOf(reviewerId,User.class);
		//todo: 人员审核人资质的检查：
		Assert.notNull(reviewer, "未找到user:"+reviewerId);
		User approver = entityOf(approverId,User.class);
		//todo: 人员审批人资质的检查：到了执行审批操作环节还要进行资格审查。
		Assert.notNull(approver, "未找到user:"+approverId);
		//一个任务：有多个设备同一批次一起检验的。
        //多台设备捆绑，某一个设备报错了，其它设备也是不能改。
		task.getDets().forEach(detail -> {
			Isp isp =detail.getIsp();
			//初始化报告 模板选定：这个环节：不允许还没有建立 Report 对象,否则玩不转。把Report的生成时间往后拖。
			if(null==isp.getReport()) {
				//未挂接具体设备的Isp:Detail只能唯一?。 Task->[isp]多个设备->Detail:每个设备单独的Isp(主报告单个的),但是针对管道可允许1个Task底下的多设备的两个设备可能装置识别码会一样的。
				Report report = new Report();     //主报告必须有且唯一一个
				//这个时间点：必须初始化Report + ApprovalStm 了，后面紧接着就是报告编制环节了，都要求前置ApprovalStm流转状态机条件。
				ApprovalStm approvalStm = new ApprovalStm();
				//派工出去，立刻就能进入编制录入原始记录的状态了，不控制实际的作业时间，何时才能开始编制报告?任务日期。
				approvalStm.setMaster(verify);		//负责启动zeebe流转
				approvalStm.setAuthr(ispMens);		//要签字的多个检验员
				approvalStm.setReviewer(reviewer);
				approvalStm.setApprover(approver);
				approvalStms.save(approvalStm);

				report.setIsp(isp);
				report.setStm(approvalStm);
				//后端和计费模块已经不关心 模板类型了。任务负责人自己决定报告模板吧。
				//后端根本不关心这些啊： todo：主报告：报告模板的分类识别代码 模板版本号？听从前端
				//主体报告模板选配：后续允许更正： Task下面全部设备的单次检验任务，都用同一个报告模板号和业务类型都一致的,#设备类别都相同的?。
				report.setModeltype(modeltype);			//"EL-DJ"
				report.setModelversion(modelversion);	 	//前端的代码管理模板的代码编号，需听从前端来匹配后端编码。 "1"
				reports.save(report);
				//这个时间点：必须设置好主报告了，不然后面无法运作： 业务上 报告编制，流转， 虽然收费实际和报告并没有直接关系的。
				isp.setReport(report);		 //主报告的！		分项报告需要单独申请发起的;
			} else {
                //重复派工的，强行更正的？
                Report report =isp.getReport();
                ApprovalStm approvalStm =report.getStm();
                if(null==approvalStm){
                    approvalStm = new ApprovalStm();
                }else {
                    Long existZeebeId= approvalStm.getPrId();
                    //已进入流转步骤：报告编辑下结论都做完了，到了校核人提请签名环节的时间点。不能轻易修改已经发动的zeebe流程数据。
                    Assert.isNull(existZeebeId, "报告审核流程已发起就不能改了,报告号"+isp.getNo());
                }
                approvalStm.setMaster(verify);		//负责启动zeebe流转
                approvalStm.setAuthr(ispMens);		//要签字的多个检验员
                approvalStm.setReviewer(reviewer);
                approvalStm.setApprover(approver);
                approvalStms.save(approvalStm);

                report.setIsp(isp);
                report.setStm(approvalStm);
                report.setModeltype(modeltype);			//"EL-DJ"
                report.setModelversion(modelversion);	 	//前端的代码管理模板的代码编号，需听从前端来匹配后端编码。 "1"
                reports.save(report);
                isp.setReport(report);
			}
			isps.save(isp);
		});
		task.setStatus(TaskState_Enum.DISP);        //允许重复派工: 已经运行的流程zeebe数据无法变更啊。
		tasks.save(task);
		return task;
	}
	/**
	 * Relay针对字段private Set<Isp>  isps=new HashSet<>(); ,DataFetchingEnvironment env
	 * 太积极了isps(int first,) 都会直接加载Isp了。怎么改都没用啊！
	 * env.getSource()? 内省情况才有的吗；多模型都能用同一个DataFetcher;
	 * 复用可能性：多个其它模型关联到Set<Isp>, 根据env.getSource()判定类型和关联字段，来反推出对方模型连接的属性名。
	 * 实际对于SimpleListConnection我这上层函数体根本不需要Relay参数的，如int first, String after都可省掉，env自己保留Relay参数。
	 * 模型名字要一致：graphql.kickstart.tools.SchemaClassScannerError: Two different classes used for type Detail:；
	 * 太多的 选择过滤？
	 * 针对属性接口分页排序(orderBy:String,asc:Boolean,where:DeviceCommonInput,first:Int,after:String,last:Int,before:String)
	 */
	@SchemaMapping
	public Connection<Detail> detail_list(Task task, @Argument String orderBy, @Argument Boolean asc, @Argument TaskDetailInput where, @Argument Integer first,
										  @Argument String after, @Argument Integer last, @Argument String before, DataFetchingEnvironment env) {
		//排序是必须的！！还允许选择过滤字段。
		DbPageConnection<Detail> connection=new DbPageConnection(env);
		//从Relay参数来反推出offset/limit;
		int offset=connection.getOffset();
		int limit=connection.getLimit();
		Pageable pageable;
		if (!StringUtils.hasLength(orderBy))
			pageable = PageOffsetFirst.of(offset, limit);
		else {
			if(null==asc)	asc=true;
			switch (orderBy) {
				case "plno" -> orderBy = "isp.dev.plno";        //隐含的关联实体
				case "used" -> orderBy = "isp.dev.used";
				case "lpho" -> orderBy = "isp.dev.lpho";
			}
			pageable = PageOffsetFirst.of(offset, limit, Sort.by(asc ? Sort.Direction.ASC : Sort.Direction.DESC, orderBy));
		}
		BooleanBuilder builder = new BooleanBuilder();
		QDetail qm = QDetail.detail;
		UUID contId;
		Class<?> ptype=env.getSource().getClass();
		//跳转接口模式: @dbpage; 从User或者Task内省而来的，并非直接给出参数字段去查询的独立接口Query模式。
		List<Detail> isps=null;     //List<Detail>  isps = new ArrayList<Detail>();
		Task parent= task;
		contId= parent.getId();
		//builder.and(qm.bus.isNotNull());   builder.and(qm.bus.task.id.eq(contId));
		builder.and(qm.task.id.eq(contId));
		if(null!=where) {
			if (StringUtils.hasText(where.getType()))
				builder.and(qm.isp.dev.type.eq(where.getType()));
			if (StringUtils.hasText(where.getSort()))
				builder.and(qm.isp.dev.sort.eq(where.getSort()));
			if (StringUtils.hasText(where.getVart()))
				builder.and(qm.isp.dev.vart.eq(where.getVart()));
			if (StringUtils.hasText(where.getSubv()))
				builder.and(qm.isp.dev.subv.eq(where.getSubv()));
			if (StringUtils.hasText(where.getPlno()))
				builder.and(qm.isp.dev.plno.eq(where.getPlno()));
			if (null != where.getUsed())
				builder.and(qm.isp.dev.used.eq(where.getUsed()));
			if (StringUtils.hasText(where.getFno()))
				builder.and(qm.isp.dev.fno.eq(where.getFno()));
			if (StringUtils.hasText(where.getPlat()))
				builder.and(qm.isp.dev.plat.eq(where.getPlat()));
			if (StringUtils.hasText(where.getCert()))
				builder.and(qm.isp.dev.cert.eq(where.getCert()));
			if (StringUtils.hasText(where.getOid()))
				builder.and(qm.isp.dev.oid.eq(where.getOid()));
			if (StringUtils.hasText(where.getCod()))
				builder.and(qm.isp.dev.cod.eq(where.getCod()));
			if (StringUtils.hasText(where.getLpho()))
				builder.and(qm.isp.dev.lpho.eq(where.getLpho()));
			if (StringUtils.hasText(where.getIdent()))
				builder.and(qm.ident.eq(where.getIdent()));
			if (null != where.getFeeOk())
				builder.and(qm.feeOk.eq(where.getFeeOk()));
			if (null != where.getStmsta())
				builder.and(qm.isp.report.stm.sta.eq(where.getStmsta()));
		}
		Slice<Detail> rpage= (Slice<Detail>)details.findAll(builder,pageable);
		isps=(List<Detail>) rpage.toList();
		//实际上SimpleListConnection也是DataFetcher<>的。内部直接提供简单cursor功能并且自己从env提取Relay参数。
		//SimpleListConnection()的输入必须是List必须是所有的node而且不能已经做了分页，
		// 给下面必须是完整Relay全部数据,SimpleListConnection自己提供游标和分页能力。
		return connection.setListData(isps).get(env);
	}
	/**取消任务： 权限，日志；当前任务状态允许取消。 若数据量大事务持续时间超过忍受程度！需考虑分解。
	 *删除数据必须先加索引。 给Eqp:isp1+isp2都加了索引之后，isp删除提速非常明显：本接口的detail+isp删除速度2.1秒/组；
	* */
	@MutationMapping
	@Transactional
	public String cancellationTask(@Argument("task") String taskId,@Argument String reason)
	{
		//两阶段提交Atomikos XA的跨多个数据库事务机制 if(!emSei.isJoinedToTransaction())   emSei.joinTransaction(); implementation 'com.atomikos:transactions-jta:5.0.9'
//		Task task=entityOf(taskId,Task.class);
//		//Task task = taskRepository.findById(taskId).orElse(null);
//		Assert.isTrue(task != null,"未找到task:"+taskId);
//		//这实体发现之间关联的情况越多的，删除就越麻烦咯，关系复杂。
//		AtomicInteger repErrCnt= new AtomicInteger();
//		//	throw new InvalidException("还有ISP关联"+taskId);
//		for (Detail detail : task.getDets()) {
//			//Detail detail= task.getDets().get(0);
//			Isp isp=detail.getIsp();
//			//Isp不管了,？有些确实需要删除的,  根据Isp的状态判定，已经终结的报告Isp不能删除。
//			if(null!=isp){
//				//isp.setBus(null);
//				//已经关联的Report的清理？
//
//				if(!isp.getReps().isEmpty())	 repErrCnt.getAndIncrement();
//				//特别的字段@ManyToMany需要清理掉： ispMen 解除关系
//				//Set<User>  mens= isp.getIspMen();
//				//mens.forEach(a -> a.getIsp().remove(isp));
//				//若不解除关系不能立即给正确的应答，在缓存期限时间内做关联查找会报错某关联id找不到。
//				//下面这两行若去掉一个都会导致关联id找不到，除非cache缓存时间过了才行，或者其他操作影响。
//				//isp.getTask().getIsps().remove(isp);
//				//爆慢时存在的注解Isp @OneToOne(cascade = CascadeType.ALL, fetch=FetchType.LAZY) Detail bus;
//				emSei.remove(isp);  //CascadeType isp物理表bus_id跟随自动删除detail，而我则后面再做的remove(detail);所以不能直接remove(isp)？【实际自动】关联删除detail？爆慢！！
//				//emSei.flush();
//			}
//			//清理关联的Charging? 收费计算信息清理。对方表有我放的ID存储着，报错！不能简单删除关联id;【麻烦】一个连接着另外一个id;连串清理。
//			//清理关联Charging.pipus<>
//			detail.getFees().forEach(a -> {
//				List<PipingUnit> pipingUnits=a.getPipus();
//				//Assert.isTrue(pipingUnits.isEmpty(),"关联Charging.pipus未清理");
//				//多对多@ManyToMany反而更容易清理：可自动删除掉中间表数据。
//				emSei.remove(a);
//			} );
//			//被引用的：管道单元锁要清除 pipus，不然就报错了。
//			detail.getPipus().forEach(a -> {
//				a.setDet(null);
//			} );
//			emSei.remove(detail);
//		//有remove(isp);没有remove(detail);也很慢的，但是detail并没有删除，可能锁冲突问题，爆慢。有remove(detail);同样慢，删了detail;
//	//没有remove(isp);有remove(detail); 就快很多！，#但是Isp保留;
//			//emSei.flush();
//			log.info("已删除-管理漏 校对的：Isp={} Bus={}",isp==null? '无':isp.getId(),detail.getId());
//			//if(repErrCnt.get()>50)  break;
//		}
//		if(0==repErrCnt.get()){
//			Assert.isTrue(task.getDets().isEmpty(),"还有ISP关联"+taskId);
//			emSei.remove(task);
//			emSei.flush();
//			return	"";
//		}
//		else
			return "";// "这次清理关联Report有"+repErrCnt.get()+"条";
	}
	/** 为任务添加设备
	 不能用ID类型jakarta.persistence@Id (, Id devId) Cannot deserialize Class jakarta.persistence.Id (of type annotation) as a Bean
	 * */
	@MutationMapping
	@Transactional
	public Task addDeviceToTask(@Argument("task") String taskId, @Argument("dev") String devId,@Argument String repNo) {
		//分布式事务机制 if(!emSei.isJoinedToTransaction())      emSei.joinTransaction();
		Task task=entityOf(taskId,Task.class);
		Assert.isTrue(task != null, "未找到Task:" + taskId);
		Eqp eqp=entityOf(devId,Eqp.class);
		Assert.isTrue(eqp != null, "未找到Eqp:" + devId);
		Isp isp = new Isp();
		isp.setDev(eqp);
		isp.setNo(repNo);	 	//报告编码序号让前端和客户自己敲定，后端唯一性检查即可
		Detail detail = new Detail();
		isp.setBus(detail);
		detail.setIsp(isp);
		detail.setTask(task);
		task.getDets().add(detail);      //最早模型设想 task.getDevs().add(eQP);
		isps.save(isp);
		details.save(detail);
		tasks.save(task);
		return task;
	}
	/**方便前端，为避免混淆，不清楚尽量不要用！ 任务下多设备校验的典型首要报告的流转状态机(只读的)。
	 * 提取生成关联的数据Task-Detail[0]-Isp-Report-stm-author[]；检验员人员初始化依据第一个Isp设备的主报告而定。做个短路ApprovalStm快捷访问。
	 * */
	@SchemaMapping(field="typicstm")
	public ApprovalStm  typicstm(Task task, DataFetchingEnvironment env) {
		if(null!=task.getDets() && task.getDets().size()>0 && null!=task.getDets().get(0) )
		{
			Isp isp=task.getDets().get(0).getIsp();		//单Task底下的多个设备的抽取象征性的主要检验报告
			if(null!=isp && null!=isp.getReport())
				return isp.getReport().getStm();
		}
		return null;
	}
	/**终结整个任务。
	 * */
	@MutationMapping
	@Transactional
	public Task finishTask(@Argument String taskId)
	{
		User user=checkAuth();
		Task task=entityOf(taskId,Task.class);
		Assert.isTrue(task != null,"未找到task:"+taskId);
		Assert.isTrue(user==task.getLiabler(),"非责任本人");
		Assert.isTrue(TaskState_Enum.DISP == task.getStatus(),"任务状态错");
		//Task必然最少一条Detail=1个检验Isp{report...}
		task.getDets().forEach(bus -> {
			bus.getIsp().getReps().forEach(report -> {
				Assert.isTrue(Procedure_Enum.END.equals(report.getStm().getSta()) || Procedure_Enum.CANCEL.equals(report.getStm().getSta()),"流程未终结");
			});
			Eqp eqp=bus.getIsp().getDev();
			String which= null==eqp? bus.getIdent() : StringUtils.hasText(eqp.getCod())? eqp.getCod(): eqp.getOid();
			Assert.isTrue(bus.isFeeOk(),"请确认收费:"+which);
		});
		//todo：【发票额以及实际回款的金额】必须大于等于 task.getCharge(); 放在哪一环节控制的, 关联委托协议合同或法定收费？
		Assert.isTrue(task.isFeeOk(),"请确认任务收费完成,预期实际收费:"+task.getCharge()+"元");
		task.setStatus(TaskState_Enum.DONE);
		tasks.save(task);
		return task;
	}
	/**方便前端，为避免混淆，不清楚尽量不要用！ 任务下多设备校验的典型首要报告的流转状态机(只读的)。
	 * 提取生成关联的数据Task-Detail[0]-Isp-Report-stm-author[]；检验员人员初始化依据第一个Isp设备的主报告而定。做个短路ApprovalStm快捷访问。
	 * */
	@SchemaMapping(field="eqpcnt")
	public Integer  eqpcnt(Task task) {
		if(null!=task.getDets())	return task.getDets().size();
		else return 0;
	}
	/**前端手动对Task底下的待做业务单即Details进行划转删除分立。
	 * */
	@MutationMapping
	@Transactional
	public TaskComResp cudTaskDetails(@Argument String id, @Argument Ifop_Enu opt,@Argument("dets") List<String> detIds, @Argument String newId) {
		TaskComResp resp=new TaskComResp();
		User user=checkAuth();
		Task task=entityOf(id,Task.class);
		assertNotNull(task, () -> "没任务");
		Task newTask=entityOf(newId,Task.class);
		List<Detail> detsTodo = new ArrayList<>();
		detIds.forEach(item -> {
			Detail detail=entityOf(item,Detail.class);
			assertTrue(task.getDets().contains(detail), () -> "没有该项检验"+item);
			detsTodo.add(detail);
		});
		assertTrue(!detsTodo.isEmpty(), () -> "没待处理业务单");
		if(null==newTask && Ifop_Enu.ADD.equals(opt)) {
			assertTrue(TaskState_Enum.INIT.equals(task.getStatus()) || TaskState_Enum.DEPART.equals(task.getStatus()) || TaskState_Enum.OFFICE.equals(task.getStatus()), () -> "状态不支持拆分");
			newTask= Task.builder().bsType(task.getBsType()).servu(task.getServu()).entrust(task.getEntrust())
					.date(task.getDate()).ispu(task.getIspu()).dep(task.getDep()).office(task.getOffice())
					.crman(task.getCrman()).crtime(LocalDateTime.now()).status(task.getStatus())
					.dets(detsTodo).build();
			Task finalNewTask = newTask;
			detsTodo.forEach(item -> {
				item.setTask(finalNewTask);
			});
			tasks.save(newTask);
			task.getDets().removeAll(detsTodo);	 	//需要 ？当前接口就刷新。
		}
		else if(null!=newTask && Ifop_Enu.UPD.equals(opt)) {
			//校验划转任务的任务容受性。同一个受检验单位？ JPA实体代理?不能直接比较？ assertTrue(newTask.getServu().equals(task.getServu())会报错！
			assertTrue(newTask.getServu().getId().equals(task.getServu().getId()), () -> "非同一受检验单位");
			Task finalNewTask1 = newTask;
			detsTodo.forEach(item -> {
				item.setTask(finalNewTask1);
			});
			newTask.getDets().addAll(detsTodo);
			task.getDets().removeAll(detsTodo);
		}
		else if(Ifop_Enu.DEL.equals(opt)) {
			//权限检查 or 审批？ 特别地放入日志。
			List<Isp>  ispsTodo = new ArrayList<>();
			detsTodo.forEach(det -> {
				if(null==det.getIsp())	 return;
				assertTrue(det.getIsp().getReps().isEmpty(), () -> "关联报告未清除,detId:"+det.getId());
				ispsTodo.add(det.getIsp());	  	//永久库清理
			});
			isps.deleteAll(ispsTodo);
			details.deleteAll(detsTodo);
			task.getDets().removeAll(detsTodo);
		}
		resp.setTask(task);
		resp.setNewTask(newTask);
		return resp;
	}
}

